/**
 * $RCSfile$
 * $Revision: 11388 $
 * $Date: 2009-11-08 18:26:55 -0600 (Sun, 08 Nov 2009) $
 *
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jawa.core.util;

import java.text.DateFormat;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A set of methods for retrieving and converting locale specific strings and numbers.
 *
 * @author Jive Software
 */
public class LocaleUtils {

	private static final Logger Log = LoggerFactory.getLogger(LocaleUtils.class);

	private static final Map<Locale, String[][]> timeZoneLists = new ConcurrentHashMap<Locale, String[][]>();

	// The basename to use for looking up the appropriate resource bundles
	// TODO - extract this out into a test that grabs the resource name from JawaGlobals
	// TODO and defaults to openfire_i18n if nothing set.
	private static final String resourceBaseName = "openfire_i18n";

	private LocaleUtils() {
	}

	/**
	 * Converts a locale string like "en", "en_US" or "en_US_win" to a Java
	 * locale object. If the conversion fails, null is returned.
	 *
	 * @param localeCode the locale code for a Java locale. See the {@link java.util.Locale}
	 *                   class for more details.
	 * @return The Java Locale that matches the locale code, or <tt>null</tt>.                  
	 */
	public static Locale localeCodeToLocale(String localeCode) {
		Locale locale = null;
		if (localeCode != null) {
			String language = null;
			String country = null;
			String variant = null;
			StringTokenizer tokenizer = new StringTokenizer(localeCode, "_");
			if (tokenizer.hasMoreTokens()) {
				language = tokenizer.nextToken();
				if (tokenizer.hasMoreTokens()) {
					country = tokenizer.nextToken();
					if (tokenizer.hasMoreTokens()) {
						variant = tokenizer.nextToken();
					}
				}
			}
			locale = new Locale(language, ((country != null) ? country : ""), ((variant != null) ? variant : ""));
		}
		return locale;
	}

	// The list of supported timezone ids. The list tries to include all of the relevant
	// time zones for the world without any extraneous zones.
	private static String[] timeZoneIds = new String[] { "GMT", "Pacific/Apia", "HST", "AST", "America/Los_Angeles",
			"America/Phoenix", "America/Mazatlan", "America/Denver", "America/Belize", "America/Chicago",
			"America/Mexico_City", "America/Regina", "America/Bogota", "America/New_York", "America/Indianapolis",
			"America/Halifax", "America/Caracas", "America/Santiago", "America/St_Johns", "America/Sao_Paulo",
			"America/Buenos_Aires", "America/Godthab", "Atlantic/South_Georgia", "Atlantic/Azores",
			"Atlantic/Cape_Verde", "Africa/Casablanca", "Europe/Dublin", "Europe/Berlin", "Europe/Belgrade",
			"Europe/Paris", "Europe/Warsaw", "ECT", "Europe/Athens", "Europe/Bucharest", "Africa/Cairo",
			"Africa/Harare", "Europe/Helsinki", "Asia/Jerusalem", "Asia/Baghdad", "Asia/Kuwait", "Europe/Moscow",
			"Africa/Nairobi", "Asia/Tehran", "Asia/Muscat", "Asia/Baku", "Asia/Kabul", "Asia/Yekaterinburg",
			"Asia/Karachi", "Asia/Calcutta", "Asia/Katmandu", "Asia/Almaty", "Asia/Dhaka", "Asia/Colombo",
			"Asia/Rangoon", "Asia/Bangkok", "Asia/Krasnoyarsk", "Asia/Hong_Kong", "Asia/Irkutsk", "Asia/Kuala_Lumpur",
			"Australia/Perth", "Asia/Taipei", "Asia/Tokyo", "Asia/Seoul", "Asia/Yakutsk", "Australia/Adelaide",
			"Australia/Darwin", "Australia/Brisbane", "Australia/Sydney", "Pacific/Guam", "Australia/Hobart",
			"Asia/Vladivostok", "Pacific/Noumea", "Pacific/Auckland", "Pacific/Fiji", "Pacific/Tongatapu" };

	// A mapping from the supported timezone ids to friendly english names.
	private static final Map<String, String> nameMap = new HashMap<String, String>();

	static {
		nameMap.put(timeZoneIds[0], "International Date Line West");
		nameMap.put(timeZoneIds[1], "Midway Island, Samoa");
		nameMap.put(timeZoneIds[2], "Hawaii");
		nameMap.put(timeZoneIds[3], "Alaska");
		nameMap.put(timeZoneIds[4], "Pacific Time (US & Canada); Tijuana");
		nameMap.put(timeZoneIds[5], "Arizona");
		nameMap.put(timeZoneIds[6], "Chihuahua, La Pax, Mazatlan");
		nameMap.put(timeZoneIds[7], "Mountain Time (US & Canada)");
		nameMap.put(timeZoneIds[8], "Central America");
		nameMap.put(timeZoneIds[9], "Central Time (US & Canada)");
		nameMap.put(timeZoneIds[10], "Guadalajara, Mexico City, Monterrey");
		nameMap.put(timeZoneIds[11], "Saskatchewan");
		nameMap.put(timeZoneIds[12], "Bogota, Lima, Quito");
		nameMap.put(timeZoneIds[13], "Eastern Time (US & Canada)");
		nameMap.put(timeZoneIds[14], "Indiana (East)");
		nameMap.put(timeZoneIds[15], "Atlantic Time (Canada)");
		nameMap.put(timeZoneIds[16], "Caracas, La Paz");
		nameMap.put(timeZoneIds[17], "Santiago");
		nameMap.put(timeZoneIds[18], "Newfoundland");
		nameMap.put(timeZoneIds[19], "Brasilia");
		nameMap.put(timeZoneIds[20], "Buenos Aires, Georgetown");
		nameMap.put(timeZoneIds[21], "Greenland");
		nameMap.put(timeZoneIds[22], "Mid-Atlantic");
		nameMap.put(timeZoneIds[23], "Azores");
		nameMap.put(timeZoneIds[24], "Cape Verde Is.");
		nameMap.put(timeZoneIds[25], "Casablanca, Monrovia");
		nameMap.put(timeZoneIds[26], "Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London");
		nameMap.put(timeZoneIds[27], "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna");
		nameMap.put(timeZoneIds[28], "Belgrade, Bratislava, Budapest, Ljubljana, Prague");
		nameMap.put(timeZoneIds[29], "Brussels, Copenhagen, Madrid, Paris");
		nameMap.put(timeZoneIds[30], "Sarajevo, Skopje, Warsaw, Zagreb");
		nameMap.put(timeZoneIds[31], "West Central Africa");
		nameMap.put(timeZoneIds[32], "Athens, Istanbul, Minsk");
		nameMap.put(timeZoneIds[33], "Bucharest");
		nameMap.put(timeZoneIds[34], "Cairo");
		nameMap.put(timeZoneIds[35], "Harare, Pretoria");
		nameMap.put(timeZoneIds[36], "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius");
		nameMap.put(timeZoneIds[37], "Jerusalem");
		nameMap.put(timeZoneIds[38], "Baghdad");
		nameMap.put(timeZoneIds[39], "Kuwait, Riyadh");
		nameMap.put(timeZoneIds[40], "Moscow, St. Petersburg, Volgograd");
		nameMap.put(timeZoneIds[41], "Nairobi");
		nameMap.put(timeZoneIds[42], "Tehran");
		nameMap.put(timeZoneIds[43], "Abu Dhabi, Muscat");
		nameMap.put(timeZoneIds[44], "Baku, Tbilisi, Muscat");
		nameMap.put(timeZoneIds[45], "Kabul");
		nameMap.put(timeZoneIds[46], "Ekaterinburg");
		nameMap.put(timeZoneIds[47], "Islamabad, Karachi, Tashkent");
		nameMap.put(timeZoneIds[48], "Chennai, Kolkata, Mumbai, New Dehli");
		nameMap.put(timeZoneIds[49], "Kathmandu");
		nameMap.put(timeZoneIds[50], "Almaty, Novosibirsk");
		nameMap.put(timeZoneIds[51], "Astana, Dhaka");
		nameMap.put(timeZoneIds[52], "Sri Jayawardenepura");
		nameMap.put(timeZoneIds[53], "Rangoon");
		nameMap.put(timeZoneIds[54], "Bangkok, Hanoi, Jakarta");
		nameMap.put(timeZoneIds[55], "Krasnoyarsk");
		nameMap.put(timeZoneIds[56], "Beijing, Chongqing, Hong Kong, Urumqi");
		nameMap.put(timeZoneIds[57], "Irkutsk, Ulaan Bataar");
		nameMap.put(timeZoneIds[58], "Kuala Lumpur, Singapore");
		nameMap.put(timeZoneIds[59], "Perth");
		nameMap.put(timeZoneIds[60], "Taipei");
		nameMap.put(timeZoneIds[61], "Osaka, Sapporo, Tokyo");
		nameMap.put(timeZoneIds[62], "Seoul");
		nameMap.put(timeZoneIds[63], "Yakutsk");
		nameMap.put(timeZoneIds[64], "Adelaide");
		nameMap.put(timeZoneIds[65], "Darwin");
		nameMap.put(timeZoneIds[66], "Brisbane");
		nameMap.put(timeZoneIds[67], "Canberra, Melbourne, Sydney");
		nameMap.put(timeZoneIds[68], "Guam, Port Moresby");
		nameMap.put(timeZoneIds[69], "Hobart");
		nameMap.put(timeZoneIds[70], "Vladivostok");
		nameMap.put(timeZoneIds[71], "Magadan, Solomon Is., New Caledonia");
		nameMap.put(timeZoneIds[72], "Auckland, Wellington");
		nameMap.put(timeZoneIds[73], "Fiji, Kamchatka, Marshall Is.");
		nameMap.put(timeZoneIds[74], "Nuku'alofa");
	}

	/**
	 * Returns a list of all available time zone's as a String [][]. The first
	 * entry in each list item is the timeZoneID, and the second is the
	 * display name.<p>
	 * <p/>
	 * The list of time zones attempts to be inclusive of all of the worlds
	 * zones while being as concise as possible. For "en" language locales
	 * the name is a friendly english name. For non-"en" language locales
	 * the standard JDK name is used for the given Locale. The GMT+/- time
	 * is also included for readability.
	 *
	 * @return a list of time zones, as a tuple of the zime zone ID, and its
	 *         display name.
	 */
	public static String[][] getTimeZoneList() {
		Locale jiveLocale = JawaGlobals.getLocale();

		String[][] timeZoneList = timeZoneLists.get(jiveLocale);
		if (timeZoneList == null) {
			String[] timeZoneIDs = timeZoneIds;
			// Now, create String[][] using the unique zones.
			timeZoneList = new String[timeZoneIDs.length][2];
			for (int i = 0; i < timeZoneList.length; i++) {
				String zoneID = timeZoneIDs[i];
				timeZoneList[i][0] = zoneID;
				timeZoneList[i][1] = getTimeZoneName(zoneID, jiveLocale);
			}

			// Add the new list to the map of locales to lists
			timeZoneLists.put(jiveLocale, timeZoneList);
		}

		return timeZoneList;
	}

	/**
	 * Returns the display name for a time zone. The display name is the name
	 * specified by the Java TimeZone class for non-"en" locales or a friendly english
	 * name for "en", with the addition of the GMT offset
	 * for human readability.
	 *
	 * @param zoneID the time zone to get the name for.
	 * @param locale the locale to use.
	 * @return the display name for the time zone.
	 */
	public static String getTimeZoneName(String zoneID, Locale locale) {
		TimeZone zone = TimeZone.getTimeZone(zoneID);
		StringBuffer buf = new StringBuffer();
		// Add in the GMT part to the name. First, figure out the offset.
		int offset = zone.getRawOffset();
		if (zone.inDaylightTime(new Date()) && zone.useDaylightTime()) {
			offset += (int) JawaConstants.HOUR;
		}

		buf.append("(");
		if (offset < 0) {
			buf.append("GMT-");
		} else {
			buf.append("GMT+");
		}
		offset = Math.abs(offset);
		int hours = offset / (int) JawaConstants.HOUR;
		int minutes = (offset % (int) JawaConstants.HOUR) / (int) JawaConstants.MINUTE;
		buf.append(hours).append(":");
		if (minutes < 10) {
			buf.append("0").append(minutes);
		} else {
			buf.append(minutes);
		}
		buf.append(") ");

		// Use a friendly english timezone name if the locale is en, otherwise use the timezone id
		if ("en".equals(locale.getLanguage())) {
			String name = nameMap.get(zoneID);
			if (name == null) {
				name = zoneID;
			}

			buf.append(name);
		} else {
			buf.append(zone.getDisplayName(true, TimeZone.LONG, locale).replace('_', ' ').replace('/', ' '));
		}

		return buf.toString();
	}

	/**
	 * Returns the specified resource bundle, which is a properties file
	 * that aids in localization of skins. This method is handy since it
	 * uses the class loader that other Jive classes are loaded from (hence,
	 * it can load bundles that are stored in jive.jar).
	 *
	 * @param baseName the name of the resource bundle to load.
	 * @param locale the desired Locale.
	 * @return the specified resource bundle, if it exists.
	 */
	public static ResourceBundle getResourceBundle(String baseName, Locale locale) {
		return ResourceBundle.getBundle(baseName, locale);
	}

	/**
	 * Returns an internationalized string loaded from a resource bundle.
	 * The locale used will be the locale specified by JawaGlobals.getLocale().
	 *
	 * @param key the key to use for retrieving the string from the
	 *      appropriate resource bundle.
	 * @return the localized string.
	 */
	public static String getLocalizedString(String key) {
		Locale locale = JawaGlobals.getLocale();

		ResourceBundle bundle = ResourceBundle.getBundle(resourceBaseName, locale);

		return getLocalizedString(key, locale, null, bundle);
	}

	/**
	 * Returns an internationalized string loaded from a resource bundle using
	 * the passed in Locale.
	 *
	 * @param key the key to use for retrieving the string from the
	 *      appropriate resource bundle.
	 * @param locale the locale to use for retrieving the appropriate
	 *      locale-specific string.
	 * @return the localized string.
	 */
	public static String getLocalizedString(String key, Locale locale) {
		ResourceBundle bundle = ResourceBundle.getBundle(resourceBaseName, locale);

		return getLocalizedString(key, locale, null, bundle);
	}

	/**
	 * Returns an internationalized string loaded from a resource bundle using
	 * the locale specified by JawaGlobals.getLocale() substituting the passed
	 * in arguments. Substitution is handled using the
	 * {@link java.text.MessageFormat} class.
	 *
	 * @param key the key to use for retrieving the string from the
	 *      appropriate resource bundle.
	 * @param arguments a list of objects to use which are formatted, then
	 *      inserted into the pattern at the appropriate places.
	 * @return the localized string.
	 */
	public static String getLocalizedString(String key, List arguments) {
		Locale locale = JawaGlobals.getLocale();

		ResourceBundle bundle = ResourceBundle.getBundle(resourceBaseName, locale);
		return getLocalizedString(key, locale, arguments, bundle);
	}

	/**
	 * Returns an internationalized string loaded from a resource bundle from the passed
	 * in plugin. If the plugin name is <tt>null</tt>, the key will be looked up using
	 * the standard resource bundle.
	 *
	 * @param key the key to use for retrieving the string from the
	 *      appropriate resource bundle.
	 * @param pluginName the name of the plugin to load the require resource bundle from.
	 * @return the localized string.
	 */
	public static String getLocalizedString(String key, String pluginName) {
		return getLocalizedString(key, pluginName, null);
	}

	/**
	 * Returns an internationalized string loaded from a resource bundle from the passed
	 * in plugin. If the plugin name is <tt>null</tt>, the key will be looked up using
	 * the standard resource bundle.
	 *
	 * @param key the key to use for retrieving the string from the
	 *      appropriate resource bundle.
	 * @param pluginName the name of the plugin to load the require resource bundle from.
	 * @param arguments a list of objects to use which are formatted, then
	 *      inserted into the pattern at the appropriate places.
	 * @return the localized string.
	 */
	public static String getLocalizedString(String key, String pluginName, List arguments) {
		return getLocalizedString(key, pluginName, arguments, null, false);
	}

	/**
	 * Returns an internationalized string loaded from a resource bundle from
	 * the passed in plugin, using the passed in Locale.
	 * 
	 * If the plugin name is <tt>null</tt>, the key will be looked up using the
	 * standard resource bundle.
	 * 
	 * If the locale is <tt>null</tt>, the Jive Global locale will be used.
	 * 
	 * @param key
	 *            the key to use for retrieving the string from the appropriate
	 *            resource bundle.
	 * @param pluginName
	 *            the name of the plugin to load the require resource bundle
	 *            from.
	 * @param arguments
	 *            a list of objects to use which are formatted, then inserted
	 *            into the pattern at the appropriate places.
	 * @param locale
	 *            the locale to use for retrieving the appropriate
	 *            locale-specific string.
	 * @param fallback
	 *            if <tt>true</tt>, the global locale used by Openfire will be
	 *            used if the requested locale is not available)
	 * @return the localized string.
	 */
	public static String getLocalizedString(String key, String pluginName, List arguments, Locale locale,
			boolean fallback) {
		if (pluginName == null) {
			return getLocalizedString(key, arguments);
		}

		if (locale == null) {
			locale = JawaGlobals.getLocale();
		}
		String i18nFile = pluginName + "_i18n";

		try {
			ResourceBundle bundle = ResourceBundle.getBundle(i18nFile, locale);
			return getLocalizedString(key, locale, arguments, bundle);
		} catch (MissingResourceException mre) {
			Locale jivesLocale = JawaGlobals.getLocale();
			if (fallback && !jivesLocale.equals(locale)) {
				Log.info("Could not find the requested locale. Falling back to default locale.", mre);
				return getLocalizedString(key, pluginName, arguments, jivesLocale, false);
			}

			Log.error(mre.getMessage(), mre);
			return key;
		}
	}

	/**
	 * Retrieve the <code>ResourceBundle</code> that is used with this plugin.
	 *
	 * @param pluginName the name of the plugin.
	 * @return the ResourceBundle used with this plugin.
	 * @throws Exception thrown if an exception occurs.
	 */
	public static ResourceBundle getPluginResourceBundle(String pluginName) throws Exception {
		final Locale locale = JawaGlobals.getLocale();

		String i18nFile = pluginName + "_i18n";

		return ResourceBundle.getBundle(i18nFile, locale);
	}

	/**
	 * Returns an internationalized string loaded from a resource bundle using
	 * the passed in Locale substituting the passed in arguments. Substitution
	 * is handled using the {@link MessageFormat} class.
	 *
	 * @param key the key to use for retrieving the string from the
	 *      appropriate resource bundle.
	 * @param locale the locale to use for retrieving the appropriate
	 *      locale-specific string.
	 * @param arguments a list of objects to use which are formatted, then
	 *      inserted into the pattern at the appropriate places.
	 * @param bundle The resource bundle from which to return the localized 
	 *      string.
	 * @return the localized string.
	 */
	public static String getLocalizedString(String key, Locale locale, List arguments, ResourceBundle bundle) {
		if (key == null) {
			throw new NullPointerException("Key cannot be null");
		}
		if (locale == null) {
			locale = JawaGlobals.getLocale();
		}

		String value;

		// See if the bundle has a value
		try {
			// The jdk caches resource bundles on it's own, so we won't bother.
			value = bundle.getString(key);
			// perform argument substitutions
			if (arguments != null) {
				MessageFormat messageFormat = new MessageFormat("");
				messageFormat.setLocale(bundle.getLocale());
				messageFormat.applyPattern(value);
				try {
					// This isn't fool-proof, but it's better than nothing
					// The idea is to try and convert strings into the
					// types of objects that the formatters expects
					// i.e. Numbers and Dates
					Format[] formats = messageFormat.getFormats();
					for (int i = 0; i < formats.length; i++) {
						Format format = formats[i];
						if (format != null) {
							if (format instanceof DateFormat) {
								if (arguments.size() > i) {
									Object val = arguments.get(i);
									if (val instanceof String) {
										DateFormat dateFmt = (DateFormat) format;
										try {
											val = dateFmt.parse((String) val);
											arguments.set(i, val);
										} catch (ParseException e) {
											Log.error(e.getMessage(), e);
										}
									}
								}
							} else if (format instanceof NumberFormat) {
								if (arguments.size() > i) {
									Object val = arguments.get(i);
									if (val instanceof String) {
										NumberFormat nbrFmt = (NumberFormat) format;
										try {
											val = nbrFmt.parse((String) val);
											arguments.set(i, val);
										} catch (ParseException e) {
											Log.error(e.getMessage(), e);
										}
									}
								}
							}
						}
					}
					value = messageFormat.format(arguments.toArray());
				} catch (IllegalArgumentException e) {
					Log.error("Unable to format resource string for key: " + key + ", argument type not supported");
					value = "";
				}
			}
		} catch (java.util.MissingResourceException mre) {
			Log.warn("Missing resource for key: " + key + " in locale " + locale.toString());
			value = "";
		}

		return value;
	}

	/**
	 * Returns an internationalized String representation of the number using
	 * the default locale.
	 *
	 * @param number the number to format.
	 * @return an internationalized String representation of the number.
	 */
	public static String getLocalizedNumber(long number) {
		return NumberFormat.getInstance().format(number);
	}

	/**
	 * Returns an internationalized String representation of the number using
	 * the specified locale.
	 *
	 * @param number the number to format.
	 * @param locale the locale to use for formatting.
	 * @return an internationalized String representation of the number.
	 */
	public static String getLocalizedNumber(long number, Locale locale) {
		return NumberFormat.getInstance(locale).format(number);
	}

	/**
	 * Returns an internationalized String representation of the number using
	 * the default locale.
	 *
	 * @param number the number to format.
	 * @return an internationalized String representation of the number.
	 */
	public static String getLocalizedNumber(double number) {
		return NumberFormat.getInstance().format(number);
	}

	/**
	 * Returns an internationalized String representation of the number using
	 * the specified locale.
	 *
	 * @param number the number to format.
	 * @param locale the locale to use for formatting.
	 * @return an internationalized String representation of the number.
	 */
	public static String getLocalizedNumber(double number, Locale locale) {
		return NumberFormat.getInstance(locale).format(number);
	}
}
